package com.ease.util {

   import com.ease.util.logging.LogManager;

   import flash.sampler.getSize;
   import flash.utils.Dictionary;
   import flash.utils.getQualifiedClassName;
   import flash.utils.getTimer;

   import mx.events.FlexEvent;
   import mx.logging.ILogger;
   import mx.managers.LayoutManager;
   import mx.utils.StringUtil;

   public class ProfilingManager {
      private static var _instance:ProfilingManager;

      private static const LOG_LEVEL_STRING:String = "Profiling";

      private var _logLevel:int;
      private var _enabled:Boolean;
      private var _logCounter:int = 1;
      private var _lastTime:Number;
      private var _pendingOperations:Object =  {};
      private var _cumulativeOperations:Array = [];
      private var _cumulativeOpPending:Boolean;

      /**
       * The singleton instance.
       */
      public static function get instance():ProfilingManager {
         if (_instance == null) {
            _instance = new ProfilingManager();
         }
         return _instance;
      }

      /**
       * Whether profiling logging is enabled.
       */
      public function get enabled():Boolean {
         return _enabled;
      }

      /** @private */
      public function set enabled(value:Boolean):void {
         if (value == this.enabled) {
            return;
         }
         if (value) {
            _logLevel = LogManager.addCustomLevel(LOG_LEVEL_STRING);
            _lastTime = NaN;
            _pendingOperations = {};
            _cumulativeOperations = [];
         } else {
            logCumulativeOperations();
            LogManager.removeCustomLevel(LOG_LEVEL_STRING);
         }
         _enabled = value;
      }

      /**
       * Logs the given message with the additional profiling info.
       */
      public function log(logger:ILogger, message:String, ... rest):String {
         if (!this.enabled) {
            return null;
         }
         return logInternal(logger, message, 0, NaN, rest);
      }

      /**
       * Logs the given message with the additional profiling info.
       * Returns a unique token to be passed to <code>logEndOperation</code> to identify
       * the operation.
       */
      public function logStartOperation(logger:ILogger, message:String,
                                        ... rest):String {
         if (!this.enabled) {
            return null;
         }
         var token:String = logInternal(logger, message, _logCounter, NaN, rest);
         var invalidLayout:Boolean = LayoutManager.getInstance().isInvalid();
         _pendingOperations[token] = new PendingOperationInfo(_lastTime, invalidLayout);
         return token;
      }

      /**
       * Logs the given message with the additional profiling info.
       * The <code>token</code> parameter should be the value returned by
       * <code>logStartOperation</code>.
       */
      public function logEndOperation(logger:ILogger, token:String,
                                      message:String, ... rest):void {
         if (!this.enabled) {
            return;
         }
         var opInfo:PendingOperationInfo = _pendingOperations[token];
         if (opInfo == null) {
            logInternal(logger, message + " (** start not found!)", 0, NaN, rest);
            return;
         }
         delete _pendingOperations[token];
         logInternal(logger, message, int(token), opInfo.startTime, rest);
         if (!opInfo.invalidLayout && LayoutManager.getInstance().isInvalid()) {
            profileRendering(logger);
         }
      }

      /**
       * Start profiling a segment of a cumulative operation.  The duration of the
       * cumulative operation and message will be logged when profiling is disabled.
       *
       * @param message
       *    This string is used to identify the operation.
       */
      public function cumulativeOperationStarted(logger:ILogger, message:String):void {
         if (!this.enabled) {
            return;
         }
         var info:CumulativeOperationInfo = getCumulativeOperation(logger, message, true);
         if (!_cumulativeOpPending) {
            info.lastTimestamp = getTimer();
            _cumulativeOpPending = true;
         } else {
            info.lastTimestamp = 0;
         }
      }

      /**
       * Stop profiling a segment of a cumulative operation.  The duration of the
       * cumulative operation and message will be logged when profiling is disabled.
       *
       * @param message
       *    This string is used to identify the operation.
       */
      public function cumulativeOperationEnded(logger:ILogger, message:String):void {
         if (!this.enabled) {
            return;
         }
         var info:CumulativeOperationInfo = getCumulativeOperation(logger, message, false);
         Diagnostics.assert(info != null, "operation not found");
         if (info.lastTimestamp != 0) {
            info.totalTime += (getTimer() - info.lastTimestamp);
            ++info.count;
            _cumulativeOpPending = false;
         }
      }

      /**
       * Returns the size of the given object in bytes.
       * <p>The computation is based on the <code>getSize</code> global function, which
       * works only in the Flash debug version. So, this function returns 0 in no debug
       * builds, i.e. if <code>config::DEBUG</code> compilation variable is false.</p>
       *
       * @param o
       *    The object to measure. This function supports arrays and dictionaries, where
       *    the items objects are also measured.
       *    If this object has method called <code>getNestedSize</code>, then it is
       *    called to compute the nested properties sizes. This is because the getSize
       *    function does not operate on nested properties.
       *
       * @param deep
       *    Whether to compute the nested properties (or items in arrays and dictionaries).
       */
      public function getObjectSize(o:Object, deep:Boolean = true):int {
         var isDebug:Boolean;
//         config::DEBUG {
            isDebug = true;
//         }
         // Class objects are shared in the runtime, so they don't take extra size.
         if (!isDebug || o == null || o is Class) {
            return 0;
         }
         var totalSize:int = getSize(o);
         if (!deep || o is String) {
            return totalSize;
         }
         var size:int = 0;
         var d:Dictionary = o as Dictionary;
         if (d != null) {
            for (var key:Object in d) {
               size = getObjectSize(key);
               totalSize += size;

               var value:Object = d[key];
               size = getObjectSize(value);
               totalSize += size;
            }
            return totalSize;
         }
         if (o.hasOwnProperty("getNestedSize")) {
            size = o.getNestedSize();
            totalSize += size;
            return totalSize;
         }
         var qName:String = getQualifiedClassName(o);
         if (MixedUtil.isIndexableOrderedCollection(o)
            || StringUtils.startsWith(qName, "__AS3__.vec::Vector.")) {
            for each (var item:Object in o) {
               size = getObjectSize(item);
               totalSize += size;
            }
            return totalSize;
         }
         return totalSize;
      }

      private function logInternal(logger:ILogger, message:String, token:int,
                                   startTime:Number, parameters:Array):String {
         if (parameters != null) {
            message = StringUtil.substitute(message, parameters);
         }
         var info:String = "";
         if (token != 0) {
            info = appendToInfoStr(info, "#" + token);
         }
         var currTime:Number = getTimer();
         if (!isNaN(_lastTime) && _lastTime != currTime) {
            var gapStr:String = TimeSpan.fromMilliseconds(currTime - _lastTime).toString();
            info = appendToInfoStr(info, "gap=" + gapStr);
         }
         if (!isNaN(startTime) && startTime != currTime) {
            var deltaStr:String = TimeSpan.fromMilliseconds(currTime - startTime).toString();
            info = appendToInfoStr(info, "delta=" + deltaStr);
         }
         if (info.length != 0) {
            message += " | " + info;
         }
         _lastTime = currTime;

         logger.log(_logLevel, message);

         return String(_logCounter++);
      }

      private function appendToInfoStr(info:String, strToAppend:String):String {
         if (info.length != 0) {
            info += ", ";
         }
         return info + strToAppend;
      }

      private function profileRendering(logger:ILogger):void {
         var token:String = logStartOperation(logger, "rendering-invalid");
         var onUpdateCompete:Function = function(event:FlexEvent):void {
            LayoutManager.getInstance().removeEventListener(FlexEvent.UPDATE_COMPLETE,
               onUpdateCompete);
            logEndOperation(logger, token, "rendering-valid");
         };
         LayoutManager.getInstance().addEventListener(FlexEvent.UPDATE_COMPLETE,
            onUpdateCompete);
      }

      private function getCumulativeOperation(logger:ILogger,
                                              message:String, create:Boolean):CumulativeOperationInfo {
         var info:CumulativeOperationInfo;
         for each (info in _cumulativeOperations) {
            if (logger == info.logger && message == info.message) {
               return info;
            }
         }
         if (!create) {
            return null;
         }
         info = new CumulativeOperationInfo();
         info.logger = logger;
         info.message = message;
         _cumulativeOperations.push(info);
         return info;
      }

      private function logCumulativeOperations():void {
         trace();
         trace();
         var totalTime:Number = 0;
         for each (var info:CumulativeOperationInfo in _cumulativeOperations) {
            log(info.logger, info.message + " (time={0}ms, count={1})",
               info.totalTime, info.count);
            totalTime += info.totalTime;
         }
         trace("---total: " + totalTime + "ms");
      }
   }
}


import mx.logging.ILogger;

class PendingOperationInfo {
   public function PendingOperationInfo(time:Number, invalidLayout:Boolean) {
      this.startTime = time;
      this.invalidLayout = invalidLayout;
   }
   public var startTime:Number;
   public var invalidLayout:Boolean;
}

class CumulativeOperationInfo {
   public var count:int;
   public var logger:ILogger;
   public var message:String;
   public var lastTimestamp:int = 0;
   public var totalTime:Number = 0;
}